package redislock

import (
	"context"
	"fmt"
	"github.com/cenkalti/backoff"
	"time"
)

const (
	prefix            = "REDIS_LOCK"
	defaultExpiration = time.Second * 3
)

type RedisLock struct {
	key          string
	value        string
	expiration   time.Duration
	lockerClient LockClient
	stopDog      context.CancelFunc
}

func New(lockerClient LockClient, key string, expiration time.Duration) (*RedisLock, error) {
	if lockerClient == nil || key == "" || expiration <= 0 {
		return nil, ErrInvalidParameter
	}
	return &RedisLock{
		key:          key,
		lockerClient: lockerClient,
		expiration:   expiration,
		value:        GetProcessAndGoroutineIDStr(),
	}, nil
}

func (lock *RedisLock) Lock(ctx context.Context) error {
	//for {
	//	select {
	//	case <-ctx.Done():
	//		return false, ctx.Err()
	//	default:
	//		if ok, err := lock.tryLock(); err != nil {
	//			return false, err
	//		} else if ok {
	//			go lock.watchDog(ctx)
	//			return true, nil
	//		}
	//		time.Sleep(time.Millisecond * 100)
	//	}
	//}

	backoffStrategy := backoff.NewExponentialBackOff()
	backoffStrategy.MaxElapsedTime = lock.expiration // 设置最大重试时间为锁的过期时间
	return backoff.Retry(func() error {
		ok, err := lock.tryLock()
		if err != nil {
			return backoff.Permanent(err)
		} else if !ok {
			return fmt.Errorf("could not acquire lock")
		}
		lock.watchDog(ctx)
		return nil
	}, backoff.WithContext(backoffStrategy, ctx))
}

func (lock *RedisLock) tryLock() (bool, error) {
	res, err := lock.lockerClient.SetNX(lock.getKey(), lock.value, lock.expiration)
	if err != nil {
		return false, err
	}
	return res == 1, nil
}

func (lock *RedisLock) getKey() string {
	return fmt.Sprintf("%s:%s", prefix, lock.key)
}

func (lock *RedisLock) Unlock(ctx context.Context) error {
	if lock.stopDog != nil {
		lock.stopDog()
	}
	_, err := lock.lockerClient.Eval(LuaCheckAndDeleteLockerKey, []string{lock.getKey()}, lock.value)
	return err
}

func (lock *RedisLock) watchDog(ctx context.Context) {
	ctx, lock.stopDog = context.WithCancel(ctx)

	defer func() {
		if lock.stopDog != nil {
			lock.stopDog()
		}
	}()

	go lock.runWatchDog(ctx)
}

func (lock *RedisLock) runWatchDog(ctx context.Context) {
	timer := time.NewTicker(lock.expiration / 3)
	defer timer.Stop()

	for range timer.C {
		select {
		case <-ctx.Done():
			return
		default:
		}

		v, err := lock.lockerClient.Eval(LuaCheckAndExpireLockerKey, []string{lock.getKey()}, lock.value)
		if err != nil || v == 0 {
			return
		}
	}
}
